# 
# THIS IS MODULE ANALYZER.PY
#

import os, sys, random, popen2, time, shutil, platform
from launcher import *
from downloader import *

def parse_and_validate_input():
    """Validates the input given on the command line.
	
	Returns the given paths -- 'log', 'bundle' -- as a dictionary."""
    prefdict = unpickle_prefs()
    
    try:
        log = args[1]
    except IndexError:
        sys.exit("Usage:\n\tCrashLogAnalyzer <path to log file>")

    if not os.path.exists(log):
        print log
        sys.exit("Error: The path to the log file is incorrect.")

    bundle_binary = prefdict['builddir']
    # If the path is not to a binary, add the necessary last part.
    if bundle_binary.endswith(".app/"):
        bundle_binary += "Contents/MacOS/Opera"
    elif bundle_binary.endswith(".app"):
        bundle_binary += "/Contents/MacOS/Opera"

    if not os.path.exists(bundle_binary):
        print bundle_binary
        sys.exit("Error: The path to the bundle_binary is incorrect.")

    return {'log':log, 'binary':bundle_binary}

def open_log(path):
    """Opens the logs or exits the program if that fails.
	
	Returns the log as a list of strings."""

    try:
        file_handle = open(path)
        file_contents = file_handle.readlines()
        file_handle.close()
    except IOError, info:
        print "opening %s failed => %s" % (path, info)
        exit(1)
        
    return file_contents



def extract_data(log):
    """First sanitize()s the log, then extracts the elevant log contents."""

    sanitized_log = sanitize(log)
    import pprint

    c = 0
    data = {}
    for i, line in enumerate(sanitized_log):

        if line.startswith("Date/Time"):
            data['date_time'] = " ".join(line.split()[1:4])

        elif line.startswith("OS Version"):
            # We do this check because Unsanity crash logs include OS Version one 
            #extra time, like this: "OS Version:     10.4.9:8P2137"
            if not ":" in line[11:]: 
                if not "Mac" in line: # Tiger or below
                    data['os_version'] = "".join(line.split()[2:3])
                    data['os_build'] = line.split()[4].strip(")")

                elif "Mac" in line: # Leopard
                    data['os_version'] = line.split()[5]
                    data['os_build'] = line.split("(")[1].strip(")\n")

        elif line.startswith("Version"):
            if not len(line.split()) == 1:
                data['opera_version'] = line.split()[1]

        elif line.startswith("Build Version"):
            data['opera_build'] = line.split()[2]

        elif line.startswith("Build Info"): # Leopard
            data['opera_build'] = line.split("~")[1].strip()

        elif line.startswith("Crashed Thread:"): # Leopard
            data['thread'] = line.split()[2]

        elif line.startswith("Thread"):

            if line.startswith("Thread:"):
                data['thread'] = line.split()[1]

            if line.startswith("Thread "):

                crashed = "Thread " + data['thread'] + " Crashed:"
                crashed_with = "Thread " + data['thread'] + " crashed with"
                crashed_plus_one = "Thread " + str(int(data['thread']) + 1) + ":"

                if line.startswith(crashed):
                    start_index = i + 1
                    data['platform'] = "Intel"

                elif line.startswith(crashed_plus_one):
                    end_index = i - 1
                    data['platform'] = "Intel"

                elif line.startswith(crashed_with):

                    # Set end_index if it isn't already set at this point
                    try:
                        if end_index >= 0:
                            pass
                    except NameError:
                        end_index = i - 1

                    # Set platform
                    if "X86" in line:
                        data['platform'] = "Intel"

                    elif "PPC" in line:
                        data['platform'] = "PowerPC"

        elif line.startswith("Exception:"):
            data['exception'] = " ".join(line.split()[1:])

        elif "Exception Type:" in line: # Leopard
            data['exception'] = " ".join(line.split()[3:])

        elif "Exception Codes:" in line: # Leopard

            try:
                #data['codes'] += ", " + " ".join(line.split()[4:])
                data['codes'] = " ".join(line.split()[2:])
            except KeyError:
                data['codes'] = " ".join(line.split()[1:])

        elif line.startswith("Code"):
            if not "Code Type" in line:

                try:
                    data['codes'] += ", " + " ".join(line.split()[1:])
                except KeyError:
                    data['codes'] = " ".join(line.split()[1:])

        elif "Opera.framework" in line:
            # Referred to as 'a_ideal' in CrashReporter documentation.
            try:
                idx = 0
                while line[idx] == " ":
                    idx += 1
                # this throws an exception if the first character on the line isn't a digit
                # 'tis a hack
                int(line[idx])
                data['log_start_address'] = line.split()[0] 
            except: # if lines are broken, get the start address from the line above current
                data['log_start_address'] = sanitized_log[i-1].split()[0] 
        else:
            c += 1
            continue
    data['mangled_symbols'] = sanitized_log[start_index:end_index]

    print "%d lines of cruft skipped" % c
    return data


def sanitize(log):
    """Runs a sanity check on the log.
	
	Returns the sanitized log as a list of strings"""

    number_of_logs = 0
    valid = False

    log.reverse()

    for i, line in enumerate(log):

        # Skip all content until we reach the last log, 
        # in case of a multi-log file.
        if "**********" in line:
            number_of_logs += 1
            log = log[0:i]
            break

        # Check that it's actually an Opera log we're looking at.
        if line.startswith("Command"):
            if not line.split()[1] == "Opera":
                sys.exit("Error: This is not an Opera crash log.")

        # A simple way of checking if the log is valid as "Date/Time" 
        # will be present in any log, pasted or generated.
        if "Date/Time" in line:
            valid = True

    log.reverse()

    if valid == False:
        sys.exit("Error: Invalid log.")
    else:
        return log


# this function isn't currently in use, and should theoretically never 
# be used again, as we're hoping to implement some small client/server functionality.

def check_platform(log_platform):
    """Finds out which platform the program is running on and calls lipo() 
    if necessary. Exits if an Intel log is attempted parsed on PowerPC 
    as this is not possible."""

    if platform.processor() == "powerpc" and log_platform == "Intel":
        clean_up("NO_INSTANCE")
        sys.exit("Error: Intel logs cannot be parsed on PowerPC.")
    elif platform.processor() == "i386" and log_platform == "PowerPC":
#		lipo(binary_path)
        clean_up("NO_INSTANCE")
        sys.exit("Error: PowerPC logs cannot be parsed on Intel. See source (lipo()) for more info.")


def lipo(binary_path):
    """If necessary, runs lipo against the Opera binary in binary_path 
    and renames the original file to Opera.universal.
	
	Returns True if succeded, Fail otherwise."""

    # lipo cannot be used at this point. It's not lipo's fault, it does its 
    # job perfectly. The problem lies with vmmap which provides strange,
    # results for the memory start address when running using the 
    # thin PowerPC binary in place of the universal binary.

    binary_path_universal = binary_path + ".universal"

    shutil.move(binary_path, binary_path_universal)

#    lipo_object = subprocess.Popen(["lipo", "-thin", "ppc", binary_path_universal, "-output", binary_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)


def launch(prefdict, data, options):
    """Launches the Opera binary with the -pd (personaldir) parameter with 
    "CrashLogAnalyzer <random number>" as value.
	
	Returns a dictionary containing 'pid' and paths to 'application_support', 
	'preferences' and 'caches' (see below).
	
	Paths:
		~/Library/Application Support/Opera xxx
		~/Library/Preferences/Opera Preferences xxx
		~/Library/Caches/Opera xxx
                path to the actual opera binary being launched
	"""
    # This will probably break in OS X 10.5+. From man 1 defaults:
    # WARNING: The defaults command will be changed in an upcoming major release 
    # to only operate on preferences domains. General plist manipulation utilities 
    # will be folded into a different command-line program.
    binary_path = prefdict['builddir'] + data['opera_build'] + ".app/Contents/MacOS/Opera"
    if not os.path.exists(binary_path):
        download_build(data['opera_build'], prefdict, options)
        
    suffix = "CrashLogAnalyzer" + str(random.randint(1,1000000))

    # check to see if our build number is higher than 4158 - if it is we can 
    # use newstyle -pd for preference directory etc.if not, we need to use 
    # the old -prefssuffix
    if int(data['opera_build']) >= 4158:
        opera_object = popen2.Popen3([binary_path, "-pd", suffix])
    else:
        opera_object = popen2.Popen3([binary_path, "-prefssuffix", suffix])
    
    return {
        'pid':opera_object.pid,
        'application_support':HOME + "/Library/Application Support/Opera " + suffix,
        'preferences':HOME + "/Library/Preferences/Opera Preferences " + suffix,
        'caches':HOME + "/Library/Caches/Opera " + suffix,
        'binary_path':binary_path
        }


def memory_start_address(pid):
    """Finds the start address where Opera.framework was loaded in memory. 
    This is referred to as 'a_real' in the CrashReporter documentation.
	
	Returns a string."""

    # Necessary waiting time to assure that Opera was properly loaded into memory.
    time.sleep(3)

    vmmap = "vmmap -w " + str(pid) + " | grep 'Opera.framework' | grep '__TEXT'"
    vmmap_stdin, vmmap_stdout = os.popen2(vmmap)
    memory_start_address = "0x" + "".join(vmmap_stdout.readlines()[0]).split()[1].split("-")[0]

#	print "MEMORY ADDRESS:\t", memory_start_address
    return memory_start_address


def slide(memory_start_address, log_start_address, mangled_symbols):
    """Calculates and applies the slide if necessary.
	
	Returns the mangled_symbols."""

    memory_start_address = int(memory_start_address, 16)
    log_start_address = int(log_start_address, 16)

    slide = memory_start_address - log_start_address

    slid_symbols = []
    for symbol in mangled_symbols:
        parts = symbol.split()
        if len(parts) == 1: continue
        if parts[1] == "Opera":
            address = symbol.split()[2]
            address = int(address, 16) + slide
            parts[2] = hex(address)
        symbol = " ".join(parts)
        slid_symbols.append(symbol)

    return slid_symbols


def atos(symbol_list, pid):
    """Runs the symbol list through atos. Calls filter() to filter the 
    results and returns them.
	
	Returns a list of unmangled symbols."""
    
    #### Needs work, doesn't spit out correctly as it is.

    atos_arg_list = ["atos", "-p", str(pid)]

    symbols = []
    for symbol in symbol_list:
        symbols.append(symbol.split()[2])

    atos_arg_list.extend(symbols)

    atos_stdin, atos_stdout = os.popen2(" ".join(atos_arg_list))
    
    unmangled_symbols = atos_stdout.readlines()

    return unmangled_symbols


def filter(unmangled_symbols):
    """Runs the symbol_list through c++filt and returns the results.
	
	Returns a list."""

    filtered_symbols = []
    fil_in, fil_out = os.popen2("c++filt")
    for symbol in unmangled_symbols:
        fil_in.write(symbol)
        fil_in.flush()
        filtered_symbols.append(fil_out.readline())

#     fil_in.close()

#     while 1:
#         filsymb = fil_out.readline()
#         if filsymb == '':
#             break
#         filtered_symbols.append(filsymb)

    return filtered_symbols


def clean_up(opera_instance):
    """Kills the Opera binary, deletes the paths generated by 
    launch() and undos the renamo to Opera.universal by launch() if necessary."""

    if opera_instance == "NO_INSTANCE":
        return

    try:
        os.kill(opera_instance['pid'], 9) # 9 = total killage
    except:
        print "Note: Could not kill Opera instance."

    try:
        shutil.rmtree(opera_instance['application_support'])
    except OSError:
        print "Note: Cache folder named \"%s\" could not be removed." % opera_instance['application_support']

    try:
        shutil.rmtree(opera_instance['preferences'])
    except OSError:
        print "Note: Cache folder named \"%s\" could not be removed." % opera_instance['preferences']

    try:
        shutil.rmtree(opera_instance['caches'])
    except OSError:
        print "Note: Cache folder named \"%s\" could not be removed." % opera_instance['caches']
        #pass
#     try:
#         shutil.move(paths['binary'] + ".universal", paths['binary'])
#     except IOError:
#         pass



def unmangle_symbols(mangled_symbols, log_start_address, prefdict, data, options):
    """Umbrella function to get all the unmangled symbols back, without 
    worrying about how the innards work"""

    opera_instance = launch(prefdict, data, options)
    opera_instance['memory_start_address'] = memory_start_address(opera_instance['pid'])
    opera_instance['log_start_address'] = log_start_address

    # Copy unslid addresses to their own list so we can merge them in later
    unslid_addresses = []

    for symbol in mangled_symbols:
        if len(symbol.split()) == 1: continue
	try:
            unslid_addresses.append(symbol.split()[2])
	except:
	    print("Something fatal happened when trying to split a line... this is the line:");
	    print(symbol);
	    sys.exit(255);

    if memory_start_address != log_start_address:
        mangled_symbols = slide(opera_instance['memory_start_address'], 
                                opera_instance['log_start_address'], mangled_symbols)

    unmangled_symbols = atos(mangled_symbols, opera_instance['pid'])

    filtered_symbols = filter(unmangled_symbols)

    # Find the longest name in the list of symbols so we can space-pad it below.
    longest_name_len = 0
    for item in mangled_symbols:
        if len(item.split()[1]) > longest_name_len:
            longest_name_len = len(item.split()[1])

    complete_trace = []
    for i in xrange(len(mangled_symbols)):
        trace_line = " ".join(mangled_symbols[i].split()[:3]) + " (%s) %s" % (unslid_addresses[i], filtered_symbols[i])
        complete_trace.append(trace_line)

    # Add spaces to align columns
    for i, line in enumerate(complete_trace):
        split = line.split()
        split[0] = "%02d" % int(split[0])
        format = "%%-%ds" % longest_name_len
        split[1] = format % split[1]
        split[2] = "%-10s" % split[2]
        split[3] = "%-12s" % split[3]
        complete_trace[i] = " ".join(split)

    format = "No %%-%ds Address    (Original)   Symbol" % longest_name_len
    header = format % "Name"

    complete_trace.insert(0, header)

    clean_up(opera_instance)

    return complete_trace

def analyze(args, options):
    prefdict = unpickle_prefs()
    raw_log = open_log(args[1])
    data = extract_data(raw_log)
    check_platform(data['platform'])
    symbols = unmangle_symbols(data['mangled_symbols'], data['log_start_address'], 
                               prefdict, data, options)

    if options.outfile:
        f = open(options.outfile, "w")
    else:
        f = None

    print >> f, """
Analyzer:\tcla version %s
Date/Time:\t%s
OS X Version:\t%s (%s)
Platform:\t%s
Opera Version:\t%s (%s)
Exception:\t%s
Codes:\t\t%s
Load Address:\t%s
""" % (cla_version, data['date_time'], data['os_version'], data['os_build'], 
            data['platform'], data['opera_version'], data['opera_build'], 
            data['exception'], data['codes'], data['log_start_address'])
    
    for symbol in symbols:
        print >> f, symbol

    if f is not None: f.close()
